package com.summer.media.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.summer.commonException.XueChengException;
import com.summer.media.dto.QueryMediaParamsDto;
import com.summer.media.dto.UploadParamsDto;
import com.summer.media.dto.UploadResultDto;
import com.summer.media.mapper.MediaFilesMapper;
import com.summer.media.mapper.MediaProcessMapper;
import com.summer.media.pojo.MediaFiles;
import com.summer.media.pojo.MediaProcess;
import com.summer.media.service.MediaFileService;
import com.summer.model.PageParams;
import com.summer.model.PageResult;
import com.summer.model.dto.RestResponse;
import io.minio.*;
import io.minio.errors.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.compress.utils.IOUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;

import java.io.*;
import java.nio.file.Files;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;


// TODO 对于不能预知的异常，不必纠结，让系统异常来捕获就行，一般只处理可以预知的异常
@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {

    @Autowired
    private MediaFilesMapper mediaFilesMapper;

    @Autowired
    private MediaProcessMapper mediaProcessMapper;

    @Autowired
    private MinioClient minioClient;

    // 构造代理对象处理事务失效
    @Autowired
    private MediaFileService mediaFileServiceImpl;

    @Value("${minio.bucket.file}")
    private String bucket_mediafile;

    @Value("${minio.bucket.videoFile}")
    private String bucket_video;


    // 分页查询媒资文件
    @Override
    public PageResult<MediaFiles> queryMediaFiels(Long companyId,
                                                  PageParams pageParams,
                                                  QueryMediaParamsDto queryMediaParamsDto) {

        //构建查询条件对象
        LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();

        //分页对象
        Page<MediaFiles> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
        // 查询数据内容获得结果
        Page<MediaFiles> pageResult = mediaFilesMapper.selectPage(page, queryWrapper);
        // 获取数据列表
        List<MediaFiles> list = pageResult.getRecords();
        // 获取数据总数
        long total = pageResult.getTotal();
        // 构建结果集
        return new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize());

    }

    @Override
    public UploadResultDto uploadFile(Long companyId, @Validated UploadParamsDto uploadParamsDto, String localFileName, String objectName) {
        // 参数校验， uploadParamsDto用validation校验
        if (StringUtils.isEmpty(localFileName)){
            log.info("上传文件失败，localFileName：{}", localFileName);
            XueChengException.cast("上传文件失败，localFileName：" + localFileName);
        }

        // 上传文件到minio
        String filename = uploadParamsDto.getFilename();
        String mimeType = getMimeType(filename);

        String extension = "";
        if (filename.contains(".")){
            extension = filename.substring(filename.indexOf("."));
        }
        String md5 = getFileMd5(new File(localFileName));
        if (StringUtils.isEmpty(objectName)){
            objectName = getDefaultFolderPath() + md5 + extension;
        }
        boolean isSuccess = upload2minio(bucket_mediafile, localFileName, mimeType, objectName);
        if (!isSuccess){
            XueChengException.cast("上传文件到minio失败");
        }

        MediaFiles mediaFiles = mediaFileServiceImpl.insert2db(companyId, uploadParamsDto, md5, objectName); // TODO 若调用方法（insert2db）内抛出异常，则下面语句不会执行（相当于此方法（uploadFile）体内所有语句为try，MVC异常处理器catch）

        UploadResultDto result = new UploadResultDto();
        BeanUtils.copyProperties(mediaFiles, result);
        return result;
    }

    @Transactional
    public MediaFiles insert2db(Long companyId, UploadParamsDto uploadParamsDto, String md5, String objectName) {
        // 查询数据库看看有无重复值
        MediaFiles mediaFiles1 = mediaFilesMapper.selectById(md5);
        if (mediaFiles1 != null){
            int delete = mediaFilesMapper.deleteById(md5);
            if (delete <= 0){
                XueChengException.cast("删除图片失败");
            }
        }

        // 保存文件信息到media_file
        MediaFiles mediaFiles = new MediaFiles();
        BeanUtils.copyProperties(uploadParamsDto, mediaFiles);

        mediaFiles.setId(md5);
        mediaFiles.setCompanyId(companyId);
        mediaFiles.setFileId(md5);
        mediaFiles.setBucket(bucket_mediafile);

        mediaFiles.setFilePath(objectName);
        mediaFiles.setUrl("/" + bucket_mediafile + "/" + objectName);
        mediaFiles.setCreateDate(LocalDateTime.now());
        mediaFiles.setAuditStatus("002003");
        int insert = mediaFilesMapper.insert(mediaFiles);
        if (insert <= 0){
            XueChengException.cast("添加课程到media_files失败");  // TODO 此异常会被捕获，因此下面的return语句无论是否有都会运行
        }
        return mediaFiles;
    }

    @Transactional
    public MediaFiles insertVideo2db(Long companyId, UploadParamsDto uploadParamsDto, String md5, String objectName) {
        // 查询数据库看看有无重复值
        MediaFiles mediaFiles1 = mediaFilesMapper.selectById(md5);
        if (mediaFiles1 != null){
            XueChengException.cast("文件已存在");
        }

        // 保存文件信息到media_file
        MediaFiles mediaFiles = new MediaFiles();
        BeanUtils.copyProperties(uploadParamsDto, mediaFiles);

        mediaFiles.setId(md5);
        mediaFiles.setCompanyId(companyId);
        mediaFiles.setFileId(md5);
        mediaFiles.setBucket(bucket_video);

        mediaFiles.setFilePath(objectName);
        mediaFiles.setUrl("/" + bucket_video + "/" + objectName);
        mediaFiles.setCreateDate(LocalDateTime.now());
        mediaFiles.setAuditStatus("002003");
        int insert = mediaFilesMapper.insert(mediaFiles);
        if (insert <= 0){
            XueChengException.cast("添加课程到media_files失败");  // TODO 此异常会被捕获，因此下面的return语句无论是否有都会运行
        }

        // 添加成功后若视频不为MP4格式则添加视频信息到media_process表，用于视频转码业务
        String mimeType = getMimeType(objectName);
        if (!mimeType.equals("video/mp4")) {  // 不为MP4都要转码
            MediaProcess mediaProcess = new MediaProcess();
            BeanUtils.copyProperties(mediaFiles, mediaProcess);
            mediaProcess.setStatus("1");
            mediaProcess.setCreateDate(LocalDateTime.now());
            mediaProcess.setFailCount(0);
            mediaProcess.setUrl(null);
            int insertProcess = mediaProcessMapper.insert(mediaProcess);
            if (insertProcess <= 0) {
                XueChengException.cast("视频信息添加到media_process表失败");
            }
        }
        return mediaFiles;
    }

    @Override
    public RestResponse<Boolean> isFileExist(String fileMd5) {
        // 查询数据库与minio，判断视频文件是否存在
        // 若数据库不存在，就算minio存在也要上传
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
        if (mediaFiles == null){
            return RestResponse.success(false);
        }
        // 此时数据库存在数据，判断minio中是否存在
        String objectName = mediaFiles.getFilePath();
        if (isExistFileInMinio(bucket_video, objectName)){
            return RestResponse.success(true, "视频已存在");
        }
        // 此时数据库存在，单minio不存在
        int deleteById = mediaFilesMapper.deleteById(fileMd5);
        if (deleteById<=0){
            XueChengException.cast("数据库中视频删除失败");
        }
        return RestResponse.success(false);
    }

    @Override
    public RestResponse<Boolean> isBlockExist(String fileMd5, int chunk) {
        String blockPath = getVideoFolder(fileMd5);
        boolean existFileInMinio = isExistFileInMinio(bucket_video, blockPath + chunk);
        if (existFileInMinio){
            return RestResponse.success(true);
        }
        return RestResponse.success(false);
    }

    @Override
    public RestResponse<Boolean> uploadBlockFile(String md5, int blockName, String localTemp) {
        // 上传分块文件到minio
        String mimeType = getMimeType("");
        String objectName = getVideoFolder(md5) + blockName;
        boolean b = upload2minio(bucket_video, localTemp, mimeType, objectName);
        if (!b){
            log.error("上传分块文件 {} 失败", objectName);
            return RestResponse.validfail(false, "上传分块文件失败");
        }
        return RestResponse.success(true);
    }

    public RestResponse<Boolean> mergeBlock(Long companyId, String fileMd5, int blockNum, UploadParamsDto uploadParamsDto){
        String destObject = getVideoSaveFolder(fileMd5) + fileMd5;
        String originFile = uploadParamsDto.getFilename();
        if (originFile.contains(".")){
            // a/b/md5/md5.avi
            destObject = getVideoSaveFolder(fileMd5) + fileMd5 + originFile.substring(originFile.lastIndexOf("."));
        }
        String blockPath = getVideoFolder(fileMd5);
        List<ComposeSource> sources = new ArrayList<>();
        // 1. 合并分块
        // 分块文件信息
        for (int i = 0; i < blockNum; i++) {
            ComposeSource composeSource = ComposeSource.builder()
                    .bucket(bucket_video)
                    .object(blockPath + i)
                    .build();
            sources.add(composeSource);
        }
        try {
            ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
                    .bucket(bucket_video)   // 桶
                    .object(destObject)  // 合并后的文件
                    .sources(sources)             // 要合并的分块文件列表
                    .build();
            minioClient.composeObject(composeObjectArgs);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("ExceptionName: " + e.getClass().getName());
            log.error("[视频合并时异常], 异常信息：{}", e.getMessage());
            return RestResponse.validfail(false, "合并文件失败。");
        }

        // 2. 校验视频
        GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(bucket_video).object(destObject).build();
        try(
                FilterInputStream inputStream = minioClient.getObject(getObjectArgs); // minio查不到数据会抛出异常

        ) {
            String destFileMd5 = DigestUtils.md5Hex(inputStream);
            if (!fileMd5.equals(destFileMd5)){
                log.error("源文件与目标文件校验失败, 源文件MD5：{}，目标文件MD5：{}", fileMd5, destFileMd5);
                return RestResponse.validfail(false, "源文件与目标文件校验失败。");
            }
        } catch (Exception e) {
            e.printStackTrace();
            String name = e.getClass().getName();
            log.error("ExceptionName: " +name);
            log.error("[源文件与目标文件校验存在错误], 错误信息：{}", e.getMessage());
        }

        // 3. 视频信息入库
        MediaFiles mediaFiles = mediaFileServiceImpl.insertVideo2db(companyId, uploadParamsDto, fileMd5, destObject);

        // 4. 删除分块
        for (int i = 0; i < blockNum; i++) {
            try {
                RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder()
                        .bucket(bucket_video)
                        .object(blockPath + i)  // 删除的对项名
                        .build();
                minioClient.removeObject(removeObjectArgs);  // TODO 循环一次就要发送一次http请求，学了stream流后来优化
            } catch (Exception e) {
                e.printStackTrace();
                log.error("异常类：{}", e.getClass().getName());
                log.error("删除视频分块异常, 错误信息：{}",e.getMessage());
                return RestResponse.validfail(false, "删除分块失败。");
            }
        }

        return RestResponse.success(true); // 走到这一步表示上传成功
    }


    // 判断minio中是否存在指定文件， 存在则返回true，反则返回false
    private boolean isExistFileInMinio(String bucket, String objectName) {
        GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(bucket).object(objectName).build();
        try(
                FilterInputStream inputStream = minioClient.getObject(getObjectArgs); // minio查不到数据会抛出异常
        ) {
            return inputStream != null;
        } catch (Exception e) {
            e.printStackTrace();
            String name = e.getClass().getName();
            log.error("ExceptionName: " +name);
            log.error("[查询minion中文件是否存在错误], 错误信息：{}", e.getMessage());
            //XueChengException.cast("视频上传错误");
            return false; // 返回false表示文件不存在
        }
    }

    // 根据MD5获取文件分块目录： a/b/ab..../chunk/
    private String getVideoFolder(String fileMd5){
        return fileMd5.charAt(0) + "/" + fileMd5.charAt(1) + "/" + fileMd5 + "/" + "chunk" + "/";
    }

    // 根据MD5获取文件目录： a/b/ab..../
    private String getVideoSaveFolder(String fileMd5){
        return fileMd5.charAt(0) + "/" + fileMd5.charAt(1) + "/" + fileMd5 + "/";
    }

    // 上传文件到minio, true上传成功，否则失败
    public boolean upload2minio(String bucket, String localFileName, String mimeType, String objectName) {
        // 调用minioClient方法上传文件
        try {
            UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
                    .bucket(bucket)
                    //对象名：文件写入minio的地址
                    .object(objectName)
                    .filename(localFileName)  // 本地文件：先将上传的文件传到本地临时文件，在从本地临时文件上传到minio
                    .contentType(mimeType)    //默认根据扩展名确定文件内容类型，也可以指定
                    .build();
            minioClient.uploadObject(uploadObjectArgs);

            log.info("上传文件到minio成功");
            return true;
        } catch (Exception e) {
            log.error("【上传文件到minio失败】 " +
                     "bucket: {}, localFileName：{}， mimeType： {}， 异常信息：{}"
                    , bucket, localFileName, mimeType, e.getMessage());
            return false;
        }

    }

    @Override
    public MediaFiles getFileById(String mediaId) {
        MediaFiles mediaFiles = mediaFilesMapper.selectById(mediaId);
        if (mediaFiles == null){
            XueChengException.cast("[MediaFileServiceImpl] 媒资文件不存在");
        }
        return mediaFiles;
    }

    //根据上传文件的扩展名取出mimeType
    private String getMimeType(String originFileName) {
        ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(originFileName);
        String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//通用mimeType，字节流
        if(extensionMatch != null){
            mimeType = extensionMatch.getMimeType();
        }
        return mimeType;
    }

    //获取文件默认存储目录路径 年/月/日
    private String getDefaultFolderPath() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        return sdf.format(new Date()).replace("-", "/")+"/";
    }

    //获取文件的md5
    private String getFileMd5(File file) {
        try (FileInputStream fileInputStream = new FileInputStream(file)) {
            return DigestUtils.md5Hex(fileInputStream);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }



    /**
     * 从minio下载到本地文件
     * @param bucket 指定minio桶
     * @param objectName minio中的文件
     * @param localPath 保存到本地文件绝对路径
     * @return 返回本地文件对象
     */
    private File downloadFileFromMinIO(String bucket,String objectName, String localPath) {
        File local = new File(localPath);
        try (
                FileOutputStream outputStream = new FileOutputStream(local);
                InputStream stream = minioClient.getObject(GetObjectArgs.builder()
                    .bucket(bucket)
                .object(objectName)
                .build());
        ) {
            IOUtils.copy(stream, outputStream);
            return local;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 从minio下载到本地临时文件
     * @param bucket 指定minio桶
     * @param objectName minio中的文件
     * @return 返回本地文件对象
     */
    public File downloadFileFromMinIOToLocalTemp(String bucket,String objectName) {
        FileOutputStream outputStream = null;
        InputStream stream = null;
        try {
            // 创建临时文件的输出流对象
            File localTemp = File.createTempFile("minio", "temp");
            localTemp.deleteOnExit();
            outputStream = new FileOutputStream(localTemp);

            // 从minio获取文件输入流
            stream = minioClient.getObject(GetObjectArgs.builder()
                    .bucket(bucket)
                    .object(objectName)
                    .build());
            // 流copy
            IOUtils.copy(stream, outputStream);
            return localTemp;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }finally {
            if (stream != null) {
                try {
                    outputStream.close();
                    stream.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }


}
